/*
 * Copyright (c) 2022-2022 Huawei Technologies Co., Ltd. All rights reserved.
 * Copyright (c) 2022-2022 Huawei Device Co., Ltd. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this list of
 *    conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list
 *    of conditions and the following disclaimer in the documentation and/or other materials
 *    provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors may be used
 *    to endorse or promote products derived from this software without specific prior written
 *    permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "los_vm_mlock.h"
#include "los_vm_fault.h"
#include "los_vm_lock.h"
#include "los_vm_filemap.h"
#include "arm.h"

#define max(a, b) ((a) > (b) ? (a) : (b))

UINT32 OsCountDuplicateLockedPages(LosVmSpace *space, VADDR_T start, size_t len) {
    VADDR_T lockStart, lockEnd, regionStart, regionEnd;
    VADDR_T end = start + len;
    UINT32 lockSize;
    UINT32 count = 0;
    LosVmMapRegion *region;
    LosRbNode *pstRbNodeTemp = NULL;
    LosRbNode *pstRbNodeNext = NULL;

    (VOID)LOS_MuxAcquire(&space->regionMux);
    /* search the region list */
    RB_SCAN_SAFE(&space->regionRbTree, pstRbNodeTemp, pstRbNodeNext)
        region = (LosVmMapRegion *)pstRbNodeTemp;
        regionStart = region->range.base;
        regionEnd = region->range.base + region->range.size - 1;

        if (end < regionStart + 1 || start > regionEnd) {
            continue;
        }

        if (region->regionFlags & VM_MAP_REGION_FLAG_LOCKED) {
            lockStart = max(start, regionStart);
            lockEnd = min(regionEnd, end - 1);
            lockSize = lockEnd - lockStart + 1;
            count += lockSize;
        }
    RB_SCAN_SAFE_END(&space->regionRbTree, pstRbNodeTemp, pstRbNodeNext)
    (VOID)LOS_MuxRelease(&space->regionMux);

    return count;
}

UINT32 OsCountUnLockedPages(LosVmSpace *space) {
    UINT32 count = 0;
    LosVmMapRegion *region;
    LosRbNode *pstRbNodeTemp = NULL;
    LosRbNode *pstRbNodeNext = NULL;

    (VOID)LOS_MuxAcquire(&space->regionMux);
    /* search the region list */
    RB_SCAN_SAFE(&space->regionRbTree, pstRbNodeTemp, pstRbNodeNext)
        region = (LosVmMapRegion *)pstRbNodeTemp;

        if (!(region->regionFlags & VM_MAP_REGION_FLAG_LOCKED)) {
            count += region->range.size;
        }
    RB_SCAN_SAFE_END(&space->regionRbTree, pstRbNodeTemp, pstRbNodeNext)
    (VOID)LOS_MuxRelease(&space->regionMux);

    return count;
}

INT32 OsApplyRegionMlockFlags(LosVmSpace *space, VADDR_T start, size_t len, UINT32 flags)
{
    VADDR_T end, lockStart, lockEnd, regionStart, regionEnd;
    UINT32 lockSize, lock;
    LosVmMapRegion *region, *startRegion;
    LosRbNode *pstRbNodeTemp = NULL;
    LosRbNode *pstRbNodeNext = NULL;

    if (!IS_PAGE_ALIGNED(start) || !IS_PAGE_ALIGNED(len)) {
         return -EINVAL;
    }

    end = start + len;
    lock = !!(flags & VM_MAP_REGION_FLAG_LOCKED);

    if (end < start) {
        return -EINVAL;
    }
    if (end == start) {
        return LOS_OK;
    }

    startRegion = LOS_RegionFind(space, start);
    if (startRegion == NULL) {
        return -ENOMEM;
    }

    (VOID)LOS_MuxAcquire(&space->regionMux);
    /* search the region list */
    RB_SCAN_SAFE(&space->regionRbTree, pstRbNodeTemp, pstRbNodeNext)
        region = (LosVmMapRegion *)pstRbNodeTemp;
        regionStart = region->range.base;
        regionEnd = region->range.base + region->range.size - 1;

        if (end < regionStart + 1 || start > regionEnd) {
            continue;
        }

        if ((lock && (region->regionFlags & VM_MAP_REGION_FLAG_LOCKED)) ||
            (!lock && !(region->regionFlags & VM_MAP_REGION_FLAG_LOCKED))) {
            continue;
        }

        lockStart = max(start, regionStart);
        lockEnd = min(regionEnd, end - 1);
        lockSize = lockEnd - lockStart + 1;

        if (region->range.size > lockSize) {
            OsVmRegionAdjust(space, lockStart, lockSize);

            region = LOS_RegionFind(space, lockStart);
        }

        if (lock) {
            space->lockedPageCount += (lockSize >> PAGE_SHIFT);
        } else {
            space->lockedPageCount -= (lockSize >> PAGE_SHIFT);
        }

        region->regionFlags &= (~VM_MAP_REGION_FLAG_LOCKED);
        region->regionFlags |= flags;

    RB_SCAN_SAFE_END(&space->regionRbTree, pstRbNodeTemp, pstRbNodeNext)
    (VOID)LOS_MuxRelease(&space->regionMux);

    return LOS_OK;
}

STATIC INT32 DoRegionPopulate(LosVmSpace *space, LosVmMapRegion * region, VADDR_T start, size_t len)
{
    STATUS_T status;
    UINT32 flags;
    PADDR_T paddr;
    VADDR_T end, regionStart, regionEnd;
    LosVmPage *page;
    ExcContext frame = {
        .regCPSR = CPSR_MODE_USR,
    };

    end = start + len;
    regionStart = region->range.base;
    regionEnd = region->range.base + region->range.size - 1;

    if (end < regionStart + 1 || start > regionEnd) {
        return LOS_OK;
    }

    start = max(start, regionStart);
    end = min(regionEnd, end - 1);

    while (start <= end) {
        status = LOS_ArchMmuQuery(&space->archMmu, start, &paddr, NULL);

        if (status != LOS_OK) {
            flags = VM_MAP_PF_FLAG_USER;
            if (region->regionFlags & VM_MAP_REGION_FLAG_PERM_WRITE) {
                flags |= VM_MAP_PF_FLAG_WRITE;
            }
            if (region->regionFlags & VM_MAP_REGION_FLAG_PERM_EXECUTE) {
                flags |= VM_MAP_PF_FLAG_INSTRUCTION;
            }

            status = OsVmPageFaultHandler(start, flags, &frame);
            if (status != LOS_OK) {
                status = -ENOMEM;
                goto DONE;
            }

            status = LOS_ArchMmuQuery(&space->archMmu, start, &paddr, NULL);
            if (status != LOS_OK) {
                status = -ENOMEM;
                goto DONE;
            }
        }

        page = LOS_VmPageGet(paddr);
        if (page == NULL) {
            status = -ENOMEM;
            goto DONE;
        }

        if (region->regionFlags & VM_MAP_REGION_FLAG_LOCKED) {
            OsSetPageMlocked(page);
        }

        start += PAGE_SIZE;
    }

DONE:
    return status;
}

INT32 OsRegionPopulate(LosVmSpace *space, VADDR_T start, size_t len, INT32 ignoreErrors)
{
    STATUS_T status = LOS_OK;
    VADDR_T end;
    LosVmMapRegion *region, *startRegion;
    LosRbNode *pstRbNodeTemp = NULL;
    LosRbNode *pstRbNodeNext = NULL;

    if (!IS_PAGE_ALIGNED(start) || !IS_PAGE_ALIGNED(len)) {
         return -EINVAL;
    }

    end = start + len;

    if (end < start) {
        return -EINVAL;
    }
    if (end == start) {
        return LOS_OK;
    }

    startRegion = LOS_RegionFind(space, start);
    if (!ignoreErrors && startRegion == NULL) {
        return -EINVAL;
    }

    (VOID)LOS_MuxAcquire(&space->regionMux);
    /* search the region list */
    RB_SCAN_SAFE(&space->regionRbTree, pstRbNodeTemp, pstRbNodeNext)
        region = (LosVmMapRegion *)pstRbNodeTemp;
        status = DoRegionPopulate(space, region, start, len);
        if (status != LOS_OK) {
            if (ignoreErrors) {
                status = LOS_OK;
                continue;
            }
            break;
        }
    RB_SCAN_SAFE_END(&space->regionRbTree, pstRbNodeTemp, pstRbNodeNext)
    (VOID)LOS_MuxRelease(&space->regionMux);

    return status;
}

INT32 OsApplyVmMlockallFlags(LosVmSpace *space, UINT32 flags) {
    UINT32 lock, addFlags = 0;
    LosVmMapRegion *region;
    LosRbNode *pstRbNodeTemp = NULL;
    LosRbNode *pstRbNodeNext = NULL;

    space->deferredFlags &= (~VM_MAP_REGION_FLAG_LOCKED);
    if (flags & MCL_FUTURE) {
        space->deferredFlags |= VM_MAP_REGION_FLAG_LOCKED;
        if (!(flags & MCL_CURRENT)) {
            goto DONE;
        }
    }

    if (flags & MCL_CURRENT) {
        addFlags = VM_MAP_REGION_FLAG_LOCKED;
    }

    lock = !!(addFlags & VM_MAP_REGION_FLAG_LOCKED);

    (VOID)LOS_MuxAcquire(&space->regionMux);
    /* search the region list */
    RB_SCAN_SAFE(&space->regionRbTree, pstRbNodeTemp, pstRbNodeNext)
        region = (LosVmMapRegion *)pstRbNodeTemp;
        if ((lock && (region->regionFlags & VM_MAP_REGION_FLAG_LOCKED)) ||
            (!lock && !(region->regionFlags & VM_MAP_REGION_FLAG_LOCKED))) {
            continue;
        }
        if (lock) {
            space->lockedPageCount += (region->range.size >> PAGE_SHIFT);
        } else {
            space->lockedPageCount -= (region->range.size >> PAGE_SHIFT);
        }
        region->regionFlags &= (~VM_MAP_REGION_FLAG_LOCKED);
        region->regionFlags |= addFlags;
    RB_SCAN_SAFE_END(&space->regionRbTree, pstRbNodeTemp, pstRbNodeNext)
    (VOID)LOS_MuxRelease(&space->regionMux);

DONE:
    return LOS_OK;
}

STATIC INT32 DoRegionUnlockVmPage(LosVmSpace *space, LosVmMapRegion * region, VADDR_T start, size_t len)
{
    STATUS_T status = LOS_OK;
    PADDR_T paddr;
    VADDR_T end, regionStart, regionEnd;
    LosVmPage *page;

    end = start + len;
    regionStart = region->range.base;
    regionEnd = region->range.base + region->range.size - 1;

    if (end < regionStart + 1 || start > regionEnd) {
        return LOS_OK;
    }

    start = max(start, regionStart);
    end = min(regionEnd, end - 1);

    while (start <= end) {
        status = LOS_ArchMmuQuery(&space->archMmu, start, &paddr, NULL);
        if (status != LOS_OK) {
            start += PAGE_SIZE;
            continue;
        }

        page = LOS_VmPageGet(paddr);
        if (page == NULL) {
            status = -ENOMEM;
            return status;
        }

        if (OsIsPageMlocked(page)) {
            OsCleanPageMlocked(page);
        }

        start += PAGE_SIZE;
    }
    return status;
}

INT32 OsRegionUnlockVmPage(LosVmSpace *space, VADDR_T start, size_t len, INT32 ignoreErrors)
{
    STATUS_T status = LOS_OK;
    VADDR_T end;
    LosVmMapRegion *region, *startRegion;
    LosRbNode *pstRbNodeTemp = NULL;
    LosRbNode *pstRbNodeNext = NULL;

    if (!IS_PAGE_ALIGNED(start) || !IS_PAGE_ALIGNED(len)) {
         return -EINVAL;
    }

    end = start + len;

    if (end < start) {
        return -EINVAL;
    }

    if (end == start) {
        return LOS_OK;
    }

    startRegion = LOS_RegionFind(space, start);
    if (!ignoreErrors && startRegion == NULL) {
        return -EINVAL;
    }

    (VOID)LOS_MuxAcquire(&space->regionMux);
    /* search the region list */
    RB_SCAN_SAFE(&space->regionRbTree, pstRbNodeTemp, pstRbNodeNext)
        region = (LosVmMapRegion *)pstRbNodeTemp;
        status = DoRegionUnlockVmPage(space, region, start, len);
        if (status != LOS_OK) {
            if (ignoreErrors) {
                status = LOS_OK;
                continue;
            }
            break;
        }
    RB_SCAN_SAFE_END(&space->regionRbTree, pstRbNodeTemp, pstRbNodeNext)
    (VOID)LOS_MuxRelease(&space->regionMux);

    return status;
}
